#include "Level2.h"
#include "GLUtils.h"
#include "Exception.h"
#include "Logger.h"
#include "Transformation.h"
#include "BoxCollider.h"
#include "Behaviour.h"
#include "CameraObject.h"
#include "TerrainObject.h"
#include "Animation.h"
#include "SimpleText.h"
#include "HUDtimer.h"
#include "AudioSource.h"
#include "AudioControl.h"
#include "NetworkManager.h"
#include "Size.h"
#include "ScoreValue.h"
#include "NetworkPlayer.h"
#include "Helpers.hpp"

using namespace game_engine;
using namespace game_engine::components;
using namespace game_engine::library;
using namespace game_engine::open_gl;
using namespace game_engine::utils;

using namespace RakNet;

///////////////////////////////////////////////////////////////////////////////
// Constructors and Destructors
///////////////////////////////////////////////////////////////////////////////

Level2::Level2(void)
    : Scene("level2"), sceneManager(), isServer(false)
{
    sceneManager.setScene(this);
}

Level2::Level2(const BOOL server)
    : Scene("level2"), sceneManager(), isServer(server)
{
    sceneManager.setScene(this);
}

Level2::~Level2(void)
{
}

///////////////////////////////////////////////////////////////////////////////
// Other public methods
///////////////////////////////////////////////////////////////////////////////

const BOOL Level2::initialize(void)
{
    BOOL ok = true;
   
    try
    {
        // Create the objects of level2
        createObjects();
        // Initialize all game objects
        initGameObjects();
        // Initialize the scene manager
        sceneManager.initialize();

        // Graphics initialization
        GLUtils::commonLevelInit();
        GLUtils::levelInit(resolution.getWidth(), resolution.getHeight()); 

        // Register RPC class members
        NetworkManager *networkManager = NetworkManager::getInstance();
        RPC3_REGISTER_FUNCTION(networkManager->getRpc3Inst(), &Level2::createStar);
        RPC3_REGISTER_FUNCTION(networkManager->getRpc3Inst(), &Level2::createScoreBoard);
        RPC3_REGISTER_FUNCTION(networkManager->getRpc3Inst(), &Level2::removeObject);
        //RPC3_REGISTER_FUNCTION(networkManager->getRpc3Inst(), &Level2::createEnemy);
        RPC3_REGISTER_FUNCTION(networkManager->getRpc3Inst(), &Level2::createPlayer);

        // Play World music
        TerrainObject *world = dynamic_cast<TerrainObject*> (getObjectByName("World"));
        world->playBgm();

    } catch (exception &e) {
        Logger::FATAL(GAME_LOG, string("Error during level2 initialization : ") + 
            e.what());
        return false;
    }

    return ok;
}

const BOOL Level2::update(const FLOAT32 time)
{
    // Update the whole scene
    BOOL ok = true;
    try
    {
        // Update game objects according to network data
        NetworkManager *networkManager = NetworkManager::getInstance();
        networkManager->update();

        updateGameObjects(time);
        ok = sceneManager.update();
    } catch (exception &e)
    {
        string err("Error during scene update: ");
        err.append(e.what());
        Logger::ERR(GAME_LOG, err);
        return false;
    }

  	if(keys[ESC_KEY])
    {
        ok = false;
        keys[ESC_KEY] = false;
    }

    return ok;
}

void Level2::finalize(void)
{
    try
    {
        // Finalize game objects
        finishGameObjects();
        // Finalize the scene manager
        sceneManager.finalize();
        // Stop World music
        TerrainObject *world = dynamic_cast<TerrainObject*> (getObjectByName("World"));
        world->stopBgm();
    } catch (exception &e)
    {
        string err("Error during scene finalization: ");
        err.append(e.what());
        Logger::ERR(GAME_LOG, err);
        throw RuntimeException(err);
    }
}

//void Level2::render(void)
//{
//}

void Level2::createStar(const UINT32 index, const Point &position, RPC3 *rpcFromNetwork)
{
	if (rpcFromNetwork == 0)
    {
        Logger::INFO(GAME_LOG, "createStar called locally");
    }
	else
    {
        string info("createStar called from ");
        info.append(rpcFromNetwork->GetLastSenderAddress().ToString());
		Logger::INFO(GAME_LOG, info);
    }

    GameObject *star = new GameObject(this, "Star" + to_string<UINT32>(index, dec), 
        "Star", ENTITY_LEVEL);

    // Create its components
    Transformation *starTransf = new Transformation(star, position.getX(), 
        position.getY(), 0.0, 0.0, 1.0, 1.0);
    Animation *starAnim = new Animation(star, "animations.xml", "kill_powerup.png");
    starAnim->resetSequence();
    starAnim->setActiveSequence("idle");
    BoxCollider *starCollider = new BoxCollider(star, Size(32.0f, 40.0f));

    // Add components to object
    star->addComponent(starTransf);
    star->addComponent(starAnim);
    star->addComponent(starCollider);

    // Add objects to current scene
    addObject(star);

    // The object must be initialized if its creation has been ordered by a
    // distant machine
    if (rpcFromNetwork != 0)
    {
        star->initialize();
    }

    // Call remote procedure
  	if (rpcFromNetwork == 0)
	{
        NetworkManager *networkManager = NetworkManager::getInstance();
        (networkManager->getRpc3Inst())->CallCPP("&Level2::createStar", 
            GetNetworkID(), index, position, rpcFromNetwork);
    }
}

void Level2::createScoreBoard(RakNet::RPC3 *rpcFromNetwork)
{
    string label;
    string prefix;
    FLOAT32 offset = 0.0f;
	if (rpcFromNetwork == 0)
    {
        Logger::INFO(GAME_LOG, "createScoreBoard called locally");
        label = "PLAYER";
        prefix = "Player";
        offset = 0.0f;
    }
	else
    {
        string info("createScoreBoard called from ");
        info.append(rpcFromNetwork->GetLastSenderAddress().ToString());
		Logger::INFO(GAME_LOG, info);
        label = "ENEMY";
        prefix = "Enemy";
        offset = 310.0f;
    }

	///////////////////////////////////////////////////////////////////////////
    // Create the PLAYER label
    ///////////////////////////////////////////////////////////////////////////
    GameObject *playerLabel = new GameObject(this, prefix + "Label", "Label", HUD_LEVEL);

    // Create its components
    Transformation *plTransf = new Transformation(playerLabel, 80.0 + offset, 610.0, 
        0.0, 0.0, 1.0, 1.0);
    SimpleText *plText = new SimpleText(playerLabel, HELVETICA_12, 
        ColorRGB(255.0, 255.0, 255.0));
    plText->setText(label);

    // Add components to object
    playerLabel->addComponent(plTransf);
    playerLabel->addComponent(plText);

	///////////////////////////////////////////////////////////////////////////
    // Create the SCORE label
    ///////////////////////////////////////////////////////////////////////////
    GameObject *scoreLabel = new GameObject(this, prefix + "ScoreLabel", "Label", 
        HUD_LEVEL);

    // Create its components
    Transformation *slTransf = new Transformation(scoreLabel, 40.0 + offset, 585.0, 
        0.0, 0.0, 1.0, 1.0);
    SimpleText *slText = new SimpleText(scoreLabel, HELVETICA_12, 
        ColorRGB(255.0, 255.0, 255.0));
    slText->setText("SCORE : ");

    // Add components to object
    scoreLabel->addComponent(slTransf);
    scoreLabel->addComponent(slText);

	///////////////////////////////////////////////////////////////////////////
    // Create the SCORE value
    ///////////////////////////////////////////////////////////////////////////
    ScoreValue *scoreValue = new ScoreValue(this, prefix + "ScoreValue", "Label", 
        HUD_LEVEL, rpcFromNetwork);
    scoreValue->setPosition(Point(120.0 + offset, 585.0));
    scoreValue->setScore(0);

	/////////////////////////////////////////////////////////////////////////////
 //   // Create the LIFE label
 //   ///////////////////////////////////////////////////////////////////////////
 //   GameObject *lifeLabel = new GameObject(this, prefix + "LifeLabel", "Label", HUD_LEVEL);

 //   // Create its components
 //   Transformation *llTransf = new Transformation(lifeLabel, 40.0 + offset, 555.0, 
 //       0.0, 0.0, 1.0, 1.0);
 //   SimpleText *llText = new SimpleText(lifeLabel, HELVETICA_12, 
 //       ColorRGB(255.0, 255.0, 255.0));
 //   llText->setText("LIFES : ");

 //   // Add components to object
 //   lifeLabel->addComponent(llTransf);
 //   lifeLabel->addComponent(llText);

	/////////////////////////////////////////////////////////////////////////////
 //   // Create the LIFE value
 //   ///////////////////////////////////////////////////////////////////////////
 //   HUDlife *lifeBar = new HUDlife(this, prefix + "LifeBar", "Label", HUD_LEVEL);
 //   lifeBar->setMaxLifes(MAX_LIFES);
 //   lifeBar->setTextureFile("heart.png");
 //   lifeBar->setTextureType(GL_RGBA);
 //   lifeBar->setPosition(Point(120.0 + offset, 560.0));

    // Add objects to current scene
    addObject(playerLabel);
    addObject(scoreLabel);
    addObject(scoreValue);
    //addObject(lifeLabel);
    //addObject(lifeBar);

    // The objects must be initialized if their creation have been ordered by a
    // distant machine
    if (rpcFromNetwork != 0)
    {
        playerLabel->initialize();
        scoreLabel->initialize();
        scoreValue->initialize();
        //lifeLabel->initialize();
        //lifeBar->initialize();
    }

    // Call remote procedure
  	if (rpcFromNetwork == 0)
	{
        NetworkManager *networkManager = NetworkManager::getInstance();
        (networkManager->getRpc3Inst())->CallCPP("&Level2::createScoreBoard", 
            GetNetworkID(), rpcFromNetwork);
    }

}

void Level2::removeObject(RakString objName, RPC3 *rpcFromNetwork)
{
	if (rpcFromNetwork == 0)
    {
        Logger::INFO(GAME_LOG, "removeObject called locally");
    }
	else
    {
        string info("removeObject called from ");
        info.append(rpcFromNetwork->GetLastSenderAddress().ToString());
		Logger::INFO(GAME_LOG, info);
    }

    // Delete object
    GameObject *obj = objectsByName[string(objName.C_String())];
    objectsByName.erase(string(objName.C_String()));
    objects.remove(obj);
    delete obj;

    // Call remote procedure
  	if (rpcFromNetwork == 0)
	{
        NetworkManager *networkManager = NetworkManager::getInstance();
        (networkManager->getRpc3Inst())->CallCPP("&Level2::removeObject", 
            GetNetworkID(), objName, rpcFromNetwork);
    }

}

void Level2::createPlayer(RPC3 *rpcFromNetwork)
{
    string objName;
    FLOAT32 offsetX = 0.0f;
    Point pos(50.0f, 70.0f);

    if (rpcFromNetwork == 0)
    {
        objName = "Player";
        if (!isServer)
        {
            offsetX = 400.0f;
        }
    }
    else
    {
        objName = "Enemy";
        if (isServer)
        {
            offsetX = 400.0f;
        }
    }

    pos.setX(pos.getX() + offsetX);
    NetworkPlayer *player = new NetworkPlayer(this, objName, objName, ENTITY_LEVEL, pos, rpcFromNetwork);

    // Add objects to current scene
    addObject(player);

    // Call remote procedure
    if (rpcFromNetwork == 0)
    {
        NetworkManager *networkManager = NetworkManager::getInstance();
        (networkManager->getRpc3Inst())->CallCPP("&Level2::createPlayer", 
            GetNetworkID(), rpcFromNetwork);
    }
    else
    {
        // Initialize object on remote machine
        player->initialize();

        // Initialize the scene manager
        sceneManager.initialize();
    }
}

///////////////////////////////////////////////////////////////////////////////
// Private methods
///////////////////////////////////////////////////////////////////////////////

void Level2::createObjects(void)
{
    // TODO : order IS VERY IMPORTANT, so objects must have a priority in order
    // to make sure that methods like initialize, update, render ... are called
    // correctly

    // Create the terrain object
    createTerrain();

    // Create the player
    createPlayer(0);

    // Create the enemy on remote machine
    //createEnemy(0);

    if (isServer)
    {
        // Create the powerups
        createPowerups();
    }

    // Create the HUD
    createHUD();

    // Create the cameras
    createCameras();

    // Advanced configurations
    CameraObject *mainCamera = dynamic_cast<CameraObject*> (getObjectByName("MainCamera"));
    mainCamera->setFollowed(getObjectByName("Player"));

    // Set the active camera
    activeCamera = mainCamera;
}

void Level2::createTerrain(void)
{
    TerrainObject *world = new TerrainObject(this, "World", "World", TERRAIN_LEVEL);
    world->setDescriptors("lvl1_idx.txt", "lvl1_tiles.png");
    world->setBgm("island.ogg", true);

    // Add objects to current scene
    addObject(world);
}

//void Level2::createPlayer(void)
//{
//    GameObject *player = new GameObject(this, "Player", "Player", ENTITY_LEVEL);
//
//    // Create its components
//    FLOAT32 offsetX = 0.0f;
//    if (!isServer)
//    {
//        offsetX = 400.0f;
//    }
//    Transformation *playerTransf = new Transformation(player, 50.0 + offsetX, 70.0, 
//        0.0, 0.0, 1.0, 1.0);
//    Animation *playerAnim = new Animation(player, "animations.xml", "player.png");
//    AudioSource *playerAudio = new AudioSource(player, "footsteps", EFFECT);
//    playerAudio->setAudioFile("footsteps.ogg");
//    playerAudio->setLoop(true);
//    AudioControl *audioControl = new AudioControl(player);
//
//    BoxCollider *playerCollider = new BoxCollider(player, Size(32.0f, 64.0f));
//    ThirdPersonController *playerController = new ThirdPersonController(player);
//    playerController->setIdleAnimationId("idle");
//    playerController->setWalkAnimationId(EAST, "walkright");
//    playerController->setWalkAnimationId(NORTH, "walkup");
//    playerController->setWalkAnimationId(WEST, "walkleft");
//    playerController->setWalkAnimationId(SOUTH, "walkdown");
//    playerController->setWalkSpeed(25.0f);
//
//    // Add components to object
//    player->addComponent(playerTransf);
//    player->addComponent(playerCollider);
//    player->addComponent(playerController);
//    player->addComponent(playerAnim);
//    player->addComponent(playerAudio);
//    player->addComponent(audioControl);
//
//    // Add objects to current scene
//    addObject(player);
//}

void Level2::createPowerups(void)
{
    // Create powerups of type Star, specifying their position
    createStar(1, Point(50.0, 350.0), 0);
    createStar(2, Point(350.0, 250.0), 0);
    createStar(3, Point(450.0, 350.0), 0);
}

void Level2::createHUD(void)
{
    ///////////////////////////////////////////////////////////////////////////
    // Create a HUD timer
    ///////////////////////////////////////////////////////////////////////////
    GameObject *timerLabel = new GameObject(this, "TimerLabel", "Label", HUD_LEVEL);
    // Create its components
    Transformation *labelTransf = new Transformation(timerLabel, 220.0, 605.0, 
        0.0, 0.0, 1.0, 1.0);
    SimpleText *labelText = new SimpleText(timerLabel, TIMES_ROMAN_24, 
        ColorRGB(255.0, 255.0, 255.0));
    labelText->setText("TIME");
    // Add components to object
    timerLabel->addComponent(labelTransf);
    timerLabel->addComponent(labelText);
    
    HUDtimer *timer = new HUDtimer(this, "Time", "HUDtimer", HUD_LEVEL);
	timer->setMode(TIMER);
	timer->setValue(999.99f);
	timer->setPosition(Point(240.0f, 580.0f));

    ///////////////////////////////////////////////////////////////////////////
    // Create the scoreboard
    ///////////////////////////////////////////////////////////////////////////
    createScoreBoard(0);

    // Add objects to current scene
    addObject(timerLabel);
	addObject(timer);

}

void Level2::createCameras(void)
{
    CameraObject *mainCamera = new CameraObject(this, "MainCamera", "Camera", 
        HUD_LEVEL);
    mainCamera->setInitialPosition(Point(256.0, 320.0));
    mainCamera->setView(Rect(0.0, 0.0, resolution.getHeight(), 
        resolution.getWidth()));

    // Add object to current scene
    addObject(mainCamera);
}
